/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.context;

import java.io.IOException;
import java.util.Properties;

import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
 * 执行root application context的具体初始化方法，在 {@link ContextLoaderListener} and
 * {@link ContextLoaderServlet}中被调用。<br/>
 * 
 * Performs the actual initialization work for the root application context.
 * Called by {@link ContextLoaderListener} and {@link ContextLoaderServlet}.
 * 
 * <p>
 * Looks for a "contextClass" parameter at the web.xml context-param level to
 * specify the context class type, falling back to the default of
 * {@link org.springframework.web.context.support.XmlWebApplicationContext} if
 * not found. With the default ContextLoader implementation, any context class
 * specified needs to implement the ConfigurableWebApplicationContext interface.
 * 
 * <p>
 * Processes a "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}"
 * context-param and passes its value to the context instance, parsing it into
 * potentially multiple file paths which can be separated by any number of
 * commas and spaces, e.g. "WEB-INF/applicationContext1.xml,
 * WEB-INF/applicationContext2.xml". Ant-style path patterns are supported as
 * well, e.g. "WEB-INF/*Context.xml,WEB-INF/spring*.xml" or
 * "WEB-INF/&#42;&#42;/*Context.xml". If not explicitly specified, the context
 * implementation is supposed to use a default location (with
 * XmlWebApplicationContext: "/WEB-INF/applicationContext.xml").
 * 
 * <p>
 * Note: In case of multiple config locations, later bean definitions will
 * override ones defined in previously loaded files, at least when using one of
 * Spring's default ApplicationContext implementations. This can be leveraged to
 * deliberately override certain bean definitions via an extra XML file.
 * 
 * <p>
 * Above and beyond loading the root application context, this class can
 * optionally load or obtain and hook up a shared parent context to the root
 * application context. See the {@link #loadParentContext(ServletContext)}
 * method for more information.
 * 
 * @author Juergen Hoeller
 * @author Colin Sampaleanu
 * @since 17.02.2003
 * @see ContextLoaderListener
 * @see ContextLoaderServlet
 * @see ConfigurableWebApplicationContext
 * @see org.springframework.web.context.support.XmlWebApplicationContext
 */
public class ContextLoader {

	/**
	 * Config param for the root WebApplicationContext implementation class to
	 * use: "<code>contextClass</code>"
	 */
	public static final String CONTEXT_CLASS_PARAM = "contextClass";

	/**
	 * Name of servlet context parameter (i.e., "
	 * <code>contextConfigLocation</code>") that can specify the config location
	 * for the root context, falling back to the implementation's default
	 * otherwise.
	 * 
	 * @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
	 */
	public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";

	/**
	 * Optional servlet context parameter (i.e., "
	 * <code>locatorFactorySelector</code>") used only when obtaining a parent
	 * context using the default implementation of
	 * {@link #loadParentContext(ServletContext servletContext)}. Specifies the
	 * 'selector' used in the
	 * {@link ContextSingletonBeanFactoryLocator#getInstance(String selector)}
	 * method call, which is used to obtain the BeanFactoryLocator instance from
	 * which the parent context is obtained.
	 * <p>
	 * The default is <code>classpath*:beanRefContext.xml</code>, matching the
	 * default applied for the
	 * {@link ContextSingletonBeanFactoryLocator#getInstance()} method.
	 * Supplying the "parentContextKey" parameter is sufficient in this case.
	 */
	public static final String LOCATOR_FACTORY_SELECTOR_PARAM = "locatorFactorySelector";

	/**
	 * Optional servlet context parameter (i.e., "<code>parentContextKey</code>
	 * ") used only when obtaining a parent context using the default
	 * implementation of
	 * {@link #loadParentContext(ServletContext servletContext)}. Specifies the
	 * 'factoryKey' used in the
	 * {@link BeanFactoryLocator#useBeanFactory(String factoryKey)} method call,
	 * obtaining the parent application context from the BeanFactoryLocator
	 * instance.
	 * <p>
	 * Supplying this "parentContextKey" parameter is sufficient when relying on
	 * the default <code>classpath*:beanRefContext.xml</code> selector for
	 * candidate factory references.
	 */
	public static final String LOCATOR_FACTORY_KEY_PARAM = "parentContextKey";

	/**
	 * Name of the class path resource (relative to the ContextLoader class)
	 * that defines ContextLoader's default strategy names.
	 */
	private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";

	private static final Properties defaultStrategies;

	static {
		// Load default strategy implementations from properties file.
		// This is currently strictly internal and not meant to be customized
		// by application developers.
		try {
			ClassPathResource resource = new ClassPathResource(
					DEFAULT_STRATEGIES_PATH, ContextLoader.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		} catch (IOException ex) {
			throw new IllegalStateException(
					"Could not load 'ContextLoader.properties': "
							+ ex.getMessage());
		}
	}

	private final Log logger = LogFactory.getLog(ContextLoader.class);

	/**
	 * The root WebApplicationContext instance that this loader manages.
	 */
	private WebApplicationContext context;

	/**
	 * Holds BeanFactoryReference when loading parent factory via
	 * ContextSingletonBeanFactoryLocator.
	 */
	private BeanFactoryReference parentContextRef;

	/**
	 * 根据指定的servlet Context，初始化Spring's web application context。<br/>
	 * Initialize Spring's web application context for the given servlet
	 * context, according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
	 * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
	 * 
	 * @param servletContext
	 *            current servlet context
	 * @return the new WebApplicationContext
	 * @throws IllegalStateException
	 *             if there is already a root application context present
	 * @throws BeansException
	 *             if the context failed to initialize
	 * @see #CONTEXT_CLASS_PARAM
	 * @see #CONFIG_LOCATION_PARAM
	 */
	public WebApplicationContext initWebApplicationContext(
			ServletContext servletContext) throws IllegalStateException,
			BeansException {

		if (servletContext
				.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
			throw new IllegalStateException(
					"Cannot initialize context because there is already a root application context present - "
							+ "check whether you have multiple ContextLoader* definitions in your web.xml!");
		}

		servletContext.log("Initializing Spring root WebApplicationContext");
		if (logger.isInfoEnabled()) {
			logger.info("Root WebApplicationContext: initialization started");
		}
		long startTime = System.currentTimeMillis();

		try {
			// Determine parent for root web application context, if any.
			ApplicationContext parent = loadParentContext(servletContext);

			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
			this.context = createWebApplicationContext(servletContext, parent);
			// 在这里，将当前ApplicationContext设置到servletContext的属性中。
			servletContext
					.setAttribute(
							WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
							this.context);

			if (logger.isDebugEnabled()) {
				logger.debug("Published root WebApplicationContext as ServletContext attribute with name ["
						+ WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
						+ "]");
			}
			if (logger.isInfoEnabled()) {
				long elapsedTime = System.currentTimeMillis() - startTime;
				logger.info("Root WebApplicationContext: initialization completed in "
						+ elapsedTime + " ms");
			}

			return this.context;
		} catch (RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			servletContext
					.setAttribute(
							WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
							ex);
			throw ex;
		} catch (Error err) {
			logger.error("Context initialization failed", err);
			servletContext
					.setAttribute(
							WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
							err);
			throw err;
		}
	}

	/**
	 * 为当前loader初始化root WebApplicationContext，如果没有指定自定义context类，使用默认的context 类。<br/>
	 * 
	 * Instantiate the root WebApplicationContext for this loader, either the
	 * default context class or a custom context class if specified.
	 * <p>
	 * This implementation expects custom contexts to implement the
	 * {@link ConfigurableWebApplicationContext} interface. Can be overridden in
	 * subclasses.
	 * 
	 * @param servletContext
	 *            current servlet context
	 * @param parent
	 *            the parent ApplicationContext to use, or <code>null</code> if
	 *            none
	 * @return the root WebApplicationContext
	 * @throws BeansException
	 *             if the context couldn't be initialized
	 * @see ConfigurableWebApplicationContext
	 */
	protected WebApplicationContext createWebApplicationContext(
			ServletContext servletContext, ApplicationContext parent)
			throws BeansException {

		Class contextClass = determineContextClass(servletContext);
		if (!ConfigurableWebApplicationContext.class
				.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class ["
					+ contextClass.getName() + "] is not of type ["
					+ ConfigurableWebApplicationContext.class.getName() + "]");
		}

		ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils
				.instantiateClass(contextClass);
		wac.setParent(parent);
		wac.setServletContext(servletContext);
		String configLocation = servletContext
				.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocation != null) {
			wac.setConfigLocations(StringUtils
					.tokenizeToStringArray(
							configLocation,
							ConfigurableWebApplicationContext.CONFIG_LOCATION_DELIMITERS));
		}

		wac.refresh();
		return wac;
	}

	/**
	 * 返回要使用的WebApplicationContext的实现类，如果没有指定自定义的context类，
	 * 就使用默认的XmlWebApplicationContext。<br/>
	 * 
	 * Return the WebApplicationContext implementation class to use, either the
	 * default XmlWebApplicationContext or a custom context class if specified.
	 * 
	 * @param servletContext
	 *            current servlet context
	 * @return the WebApplicationContext implementation class to use
	 * @throws ApplicationContextException
	 *             if the context class couldn't be loaded
	 * @see #CONTEXT_CLASS_PARAM
	 * @see org.springframework.web.context.support.XmlWebApplicationContext
	 */
	protected Class determineContextClass(ServletContext servletContext)
			throws ApplicationContextException {
		String contextClassName = servletContext
				.getInitParameter(CONTEXT_CLASS_PARAM);
		if (contextClassName != null) {
			try {
				return ClassUtils.forName(contextClassName);
			} catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load custom context class ["
								+ contextClassName + "]", ex);
			}
		} else {
			contextClassName = defaultStrategies
					.getProperty(WebApplicationContext.class.getName());
			try {
				return ClassUtils.forName(contextClassName);
			} catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class ["
								+ contextClassName + "]", ex);
			}
		}
	}

	/**
	 * Template method with default implementation (which may be overridden by a
	 * subclass), to load or obtain an ApplicationContext instance which will be
	 * used as the parent context of the root WebApplicationContext. If the
	 * return value from the method is null, no parent context is set.
	 * <p>
	 * The main reason to load a parent context here is to allow multiple root
	 * web application contexts to all be children of a shared EAR context, or
	 * alternately to also share the same parent context that is visible to
	 * EJBs. For pure web applications, there is usually no need to worry about
	 * having a parent context to the root web application context.
	 * <p>
	 * The default implementation uses
	 * {@link org.springframework.context.access.ContextSingletonBeanFactoryLocator}
	 * , configured via {@link #LOCATOR_FACTORY_SELECTOR_PARAM} and
	 * {@link #LOCATOR_FACTORY_KEY_PARAM}, to load a parent context which will
	 * be shared by all other users of ContextsingletonBeanFactoryLocator which
	 * also use the same configuration parameters.
	 * 
	 * @param servletContext
	 *            current servlet context
	 * @return the parent application context, or <code>null</code> if none
	 * @throws BeansException
	 *             if the context couldn't be initialized
	 * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator
	 */
	protected ApplicationContext loadParentContext(ServletContext servletContext)
			throws BeansException {

		ApplicationContext parentContext = null;
		String locatorFactorySelector = servletContext
				.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
		String parentContextKey = servletContext
				.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);

		if (parentContextKey != null) {
			// locatorFactorySelector may be null, indicating the default
			// "classpath*:beanRefContext.xml"
			BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator
					.getInstance(locatorFactorySelector);
			if (logger.isDebugEnabled()) {
				logger.debug("Getting parent context definition: using parent context key of '"
						+ parentContextKey + "' with BeanFactoryLocator");
			}
			this.parentContextRef = locator.useBeanFactory(parentContextKey);
			parentContext = (ApplicationContext) this.parentContextRef
					.getFactory();
		}

		return parentContext;
	}

	/**
	 * Close Spring's web application context for the given servlet context. If
	 * the default {@link #loadParentContext(ServletContext)} implementation,
	 * which uses ContextSingletonBeanFactoryLocator, has loaded any shared
	 * parent context, release one reference to that shared parent context.
	 * <p>
	 * If overriding {@link #loadParentContext(ServletContext)}, you may have to
	 * override this method as well.
	 * 
	 * @param servletContext
	 *            the ServletContext that the WebApplicationContext runs in
	 */
	public void closeWebApplicationContext(ServletContext servletContext) {
		servletContext.log("Closing Spring root WebApplicationContext");
		try {
			if (this.context instanceof ConfigurableWebApplicationContext) {
				((ConfigurableWebApplicationContext) this.context).close();
			}
		} finally {
			servletContext
					.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
			if (this.parentContextRef != null) {
				this.parentContextRef.release();
			}
		}
	}

}
